package com.fingard.dsp.bank.directbank.pab02.Util.api.bean;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import com.fingard.dsp.bank.directbank.pab02.Util.api.exception.FtpErrCode;
import com.fingard.dsp.bank.directbank.pab02.Util.api.exception.FtpException;
import com.fingard.dsp.bank.directbank.pab02.Util.api.utils.Md5Alg;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;

/**
 * 文件服务器的文件处理类，处理的功能包括：
 *   1、实现服务端文件的上传和下载；
 *   2、实现客户端文件的上传和下载； 
 *   3、实现服务端和客户端文件的MD5校验功能；
 *   4、实现文件服务器的负载均衡控制处理；
 *   5、文件上传的过程中实现文件的远程重命名控制； 
 *   6、文件下载过程实现远程服务器文件读取；
 * 
 */
public class EsbFile {

	private static Log log = LogFactory.getLog(EsbFile.class);

	/* EsbFile类型，服务器 */
	public final static int SERVER = 1;
	/* EsbFile类型，客户端 */
	public final static int CLIENT = 2;

	/* EsbFile类型标志 */
	@SuppressWarnings("unused")
	private int type = -1;
	/* 文件的名称 */
	@SuppressWarnings("unused")
	private String fileName = null;
	/* 文件的绝对路径 */
	private String realFileName = null;
	/* 文件的临时文件名 */
	private String tmpFileName = null;
	/* 文件的配置文件名 */
	private String cfgFileName = null;

	/* realFileName对应的File对象 */
	private File file = null;
	/* tmpFileName对应的File对象 */
	private File tmpFile = null;
	/* cfgFileName对应的File对象 */
	private File cfgFile = null;


	/* 文件的Md5计算对象 */
	private Md5Alg md5Alm = new Md5Alg();
	/* 文件的Md5值 */
	private String fileMd5 = null;


	/* realFileName的读取流 */
	private FileInputStream reader = null;
	/* tmpFile的写入流，文件下载是客户端有效，文件上传服务端有效 */
	private FileOutputStream writer = null;
	/* cfgFile的写入流，服务端有效 */
	private FileOutputStream cfgWriter = null;
	/* cfgFile的读取流，服务端有效 */
	private FileInputStream cfgReader = null;
	/* 文件的大小，读取文件内容时有效 */
	private long size = 0;
	/* 文件的偏移量，读取文件内容时有效 */
	private long offset = 0;
	
	/**
	 * 构造函数
	 * 
	 * @param fileName
	 *            文件名称
	 * @param type
	 *            1-服务器文件，使用相对路径 2-客户端文件，使用文件绝对路径
	 * @throws FtpException
	 */
	public EsbFile(String fileName, int type) throws FtpException {
		this.type = type;
		this.fileName = fileName;
		this.realFileName = fileName;

		// 创建真实文件信息
		file = new File(realFileName);
		this.size = file.length();

		// 创建临时文件信息
		this.tmpFileName = realFileName + ".tmp";
		tmpFile = new File(tmpFileName);

		// 创建配置文件信息
		this.cfgFileName = realFileName + ".cfg";
		cfgFile = new File(cfgFileName);
	}

	/**
	 * 创建文件，用于处理文件的下载。 
	 * 
	 * @throws FtpException
	 */
	public void openForWrite() throws FtpException {

		// 删除相关文件
		this.deleteFile();

		try {
			// 目录不存在，则创建相关目录
			if (!tmpFile.getParentFile().exists())
				tmpFile.getParentFile().mkdirs();

			// 创建临时文件写入流
			writer = new FileOutputStream(tmpFile);
		} catch (Exception e) {
			if (log.isErrorEnabled())
				log.error("打开文件出错", e);
			FtpException fe = new FtpException(FtpErrCode.FileReadError, e);
			throw fe;
		}
	}

	/**
	 * 读取文件，用于处理文件的上传。
	 * 
	 * @throws FtpException
	 */
	public void openForRead(long offset) throws FtpException {

		// 本地文件，打开文件的输入流
		try {
			size = file.length();
			this.offset = offset;
			reader = new FileInputStream(realFileName);
			if (this.offset > 0)
				reader.skip(this.offset);
		} catch (FileNotFoundException e) {
			throw new FtpException(FtpErrCode.FileNotFoundError, e);
		} catch (IOException e) {
			throw new FtpException(FtpErrCode.FileReadError, e);
		}
	}
	


	/**
	 * 删除文件，在openForWrite方法中调用，�h除原始文件、临时文件
	 * 
	 * @throws FtpException
	 */
	public void deleteFile() throws FtpException {
		// 数据文件如果存在，则备份
		if (file.exists()) {
			if(file.isFile()){
				if (!file.delete()) {
					if (log.isErrorEnabled())
						log.error("删除文件[" + file + "]出错");
					throw new FtpException(FtpErrCode.FileDeleteError);
				}
				
			}else{
				if (log.isErrorEnabled())
					log.error(file+"是目录");
				throw new FtpException(FtpErrCode.NotFileError);
			}
		}

		// 临时文件如果存在，则删除
		if (tmpFile.exists()) {
			if(tmpFile.isFile()){
				if (!tmpFile.delete()) {
					if (log.isErrorEnabled())
						log.error("删除临时文件[" + tmpFile + "]出错");
					throw new FtpException(FtpErrCode.FileDeleteError);
				}
			}else{
				if (log.isErrorEnabled())
					log.error(tmpFile+"是目录");
				throw new FtpException(FtpErrCode.NotFileError);
			}
		}

		// 配置文件如果存在，则备份
		if (cfgFile.exists()) {
			if(cfgFile.isFile()){
				if (!cfgFile.delete()) {
					if (log.isErrorEnabled())
						log.error("删除配置文件[" + cfgFile + "]出错");
					throw new FtpException(FtpErrCode.FileDeleteError);
				}
			}else{
				if (log.isErrorEnabled())
					log.error(cfgFile+"是目录");
				throw new FtpException(FtpErrCode.NotFileError);
			}
		}
	}

	/**
	 * 完成文件的写入，并进行md5的校验。
	 *  md5校验成功，则将临时文件命名为真正的文件名称；
	 *  md5校验失败，则不进行临时文件的重命名，保留临时文件以备确认问题。
	 * 
	 * @param md5
	 *            远程服务器的md校验码
	 * @throws FtpException
	 */
	public void finish(String md5) throws FtpException {
		// 设置文件的Md5信息
		fileMd5 = (md5 == null ? "" : md5);

		// 关闭文件的相关流信息
		this.close();

		// 完成文件的MD5校验
		String tmpMd5 = this.getFileMd5();
		if (!tmpMd5.equals(fileMd5)) {
			if (log.isErrorEnabled())
				log.error("文件内容MD5校验失败,local=" + tmpMd5 + ",remote=" + md5);
			throw new FtpException(FtpErrCode.FileCheckError);
		}

		// 校验成功，完成文件的重命名
		if (!tmpFile.renameTo(file)) {
			if (log.isErrorEnabled())
				log.error("临时文件 [" + tmpFile + "] 转换为正式的文件 [" + file + "] 出错.");
			throw new FtpException(FtpErrCode.FileRenameError);
		}
	}

	/**
	 * 关闭文件输入流和输出流的处理
	 * 
	 * @throws FtpException
	 */
	public void close() throws FtpException {
		try {
			// 关闭文件的输入流
			if (reader != null) {
				reader.close();
				reader = null;
			}

			// 关闭文件的输出流
			if (writer != null) {
				writer.close();
				writer = null;
			}

			// 关闭配置文件的写入对象
			if (cfgWriter != null) {
				cfgWriter.close();
				cfgWriter = null;
			}

			// 关闭配置文件的读取对象
			if (cfgReader != null) {
				cfgReader.close();
				cfgReader = null;
			}
		} catch (Exception e) {
			throw new FtpException(FtpErrCode.FileCloseError, e);
		}

	}

	/**
	 * 读取文件第几个分片的内容
	 * 
	 * @param bean
	 *            输入输出参数
	 * @throws FtpException
	 */
	public void read(FileMsgBean bean) throws FtpException {
		if (log.isDebugEnabled())
			log.debug("读取文件[" + bean.getFileName() + "]的第["
					+ bean.getFileIndex() + "]个分片");

		// 计算文件的偏移量应该是多少
		int pieceNum = bean.getPieceNum();
		long offset1 = (long) (bean.getFileIndex() - 1) * pieceNum;
		// 判读当前的偏移量与目标偏移量是否相等，如果不等则重新打开文件
		if (offset != offset1) {
			if (log.isInfoEnabled())
				log.info("文件offset错误，需要重新打开");

			this.close();
			this.openForRead(offset1);
		}

		int num = 0;
		try {
			// 读取文件内容
			num = reader.read(bean.getFileCont(), 0, pieceNum);
			if (num < 0) {
				num = 0;
			}
			offset = (long) (offset + num);
			bean.setContLen(num);
			bean.setFileSize(size);

		} catch (Exception e) {
			FtpException fe = new FtpException(FtpErrCode.FileReadError, e);
			if (log.isErrorEnabled())
				log.error(fe.getMessage(), fe);
			throw fe;
		}

		// MD5校验码不存在，需要计算校验码
		if (fileMd5 == null)
			md5Alm.update(bean.getFileCont(), 0, bean.getContLen());

		// 读取的内容少于分配或是文件便宜量大于等于文件的大小时是最后一个分片
		if (num < pieceNum || offset >= size) {
			// 设置最后分片的标志
			bean.setLastPiece(true);

			// 设置文件的MD5校验码
			if (fileMd5 == null)
				fileMd5 = this.getFileMd5();
			bean.setMd5(fileMd5);
		}
	}

	/**
	 * 写入文件第几个分片的内容
	 * 写文件到本地
	 * @param bean
	 *            输入输出参数
	 * @throws FtpException
	 */
	public void write(FileMsgBean bean) throws FtpException {
		if (log.isDebugEnabled())
			log.debug("写入文件[" + bean.getFileName() + "]的第["
					+ bean.getFileIndex() + "]个分片");
            System.out.println("写入文件[" + bean.getFileName() + "]的第["
					+ bean.getFileIndex() + "]个分片");  
		try {
			md5Alm.update(bean.getFileCont(), 0, bean.getContLen());
			writer.write(bean.getFileCont(), 0, bean.getContLen());
			writer.flush();
		} catch (Exception e) {
			FtpException fe = new FtpException(FtpErrCode.FileWriteError, e);
			throw fe;
		}
	}

	/**
	 * 生成本地文件的Md5校验码信息
	 * 
	 * @return 返回本地文件的校验码信息
	 */
	public String getFileMd5() {
		byte[] args = this.md5Alm.digest();
		StringBuilder sb = new StringBuilder();
		for (int i = 0; i < args.length; i++) {
			sb.append(Integer.toHexString((0x000000ff & args[i]) | 0xffffff00).substring(6));
		}

		if (log.isInfoEnabled())
			log.info("生成文件的Md5校验码：" + sb.toString());
		return sb.toString();
	}

	/**
	 * 获取文件的大小
	 * 
	 * @return 文件的大小
	 */
	public long getSize() {
		return size;
	}

	/**
	 * 文件类的toString
	 */
	public String toString() {
		StringBuilder sb = new StringBuilder();
		sb.append("realFileName=").append(realFileName).append("");
		return sb.toString();
	}


	/**
	 * 断点续传，填充Md5数据
	 * @param bean
	 * @throws Exception
	 */
	public void fillMd5(FileMsgBean bean) throws Exception {
		// 计算文件的偏移量应该是多少
		int pieceNum = bean.getPieceNum();
		long offset1 = (long) (bean.getFileIndex() - 1) * pieceNum;
		// 判读当前的偏移量与目标偏移量是否相等，如果不等则重新打开文件
		if (offset != offset1) {
			if (log.isInfoEnabled())
				log.info("文件offset错误，需要重新打开");

			this.close();
			this.openForRead(offset1);
		}

		int num = 0;
		try {
			// 读取文件内容
			num = reader.read(bean.getFileCont(), 0, pieceNum);
			if (num < 0) {
				num = 0;
			}
			offset = (long) (offset + num);
			bean.setContLen(num);
			bean.setFileSize(size);

		} catch (Exception e) {
			FtpException fe = new FtpException(FtpErrCode.FileReadError, e);
			if (log.isErrorEnabled())
				log.error(fe.getMessage(), fe);
			throw fe;
		}

		// MD5校验码不存在，需要计算校验码
		if (fileMd5 == null)
			md5Alm.update(bean.getFileCont(), 0, bean.getContLen());
	}
	
}
